package com.powernode.controller;

import com.powernode.config.MinioConfig;
import com.powernode.dto.MergeFileDTO;
import com.powernode.dto.UserFileDTO;
import com.powernode.result.R;
import io.minio.*;
import io.minio.errors.ErrorResponseException;
import io.minio.messages.Item;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@RestController
public class FileController {

    @Resource
    private MinioConfig minioConfig;
    @Resource
    private MinioClient minioClient;

    /**
     * 获取当前用户的所有文件列表
     * @param userId 用户Id
     * @return
     */
    @GetMapping("/getFileList")
    public R getFileList(
            @RequestParam("userId") String userId){

        // 结果文件列表
        List<UserFileDTO> userFiles = new ArrayList<>();

        // 格式化时间
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        try {
            // 1. 列出用户目录下的所有MD5文件夹
            Iterable<Result<Item>> md5Folders  = minioClient.listObjects(ListObjectsArgs.builder()
                    .bucket(minioConfig.getBucket())
//                    .prefix(userId + "/") // 以userId作为文件前缀名
                    .prefix(userId + "/") // 以userId作为文件前缀名
                    .delimiter("/")  // 使用分隔符获取"文件夹"
                    .recursive(false) // 不递归
                    .build());

            // 2. 遍历每个MD5文件夹
            for (Result<Item> folderResult : md5Folders ) {
                Item item = folderResult.get();
                System.out.println(item.objectName()); // 123/6b85a40aeeb2fe044d2e1ce3ecada528/
                System.out.println(item.isDir()); // true

                // 如果当前文件是文件夹，则继续获取文件夹内部的文件
                if (item.isDir()) {
                    String md5FolderPath = item.objectName();
                    // 3. 获取MD5文件夹中的实际文件
                    Iterable<Result<Item>> files = minioClient.listObjects(ListObjectsArgs.builder()
                            .bucket(minioConfig.getBucket())
                            .prefix(md5FolderPath)
                            .recursive(false) // 不递归遍历
                            .build());

                    // 4. 将文件信息添加到结果列表
                    for (Result<Item> fileResult : files) {
                        Item fileItem = fileResult.get();
                        if (!fileItem.isDir()) {
                            UserFileDTO userFileDTO = new UserFileDTO();
                            userFileDTO.setObjectName(fileItem.objectName());                      // 完整对象路径
                            userFileDTO.setFileMd5(fileItem.objectName().split("/")[1]);     // 文件MD5值
                            userFileDTO.setFileName(fileItem.objectName().split("/")[2]);    // 文件名
                            userFileDTO.setFileSize(fileItem.size());                              // 文件大小
                            Instant instant = fileItem.lastModified().toInstant();
                            Date date = Date.from(instant);
                            userFileDTO.setLastModified(sdf.format(date));                         // 最后修改时间

                            userFiles.add(userFileDTO);
                        }
                    }
                } else { // 普通上传的文件
                    UserFileDTO userFileDTO = new UserFileDTO();
                    userFileDTO.setObjectName(item.objectName());                               // 完整对象路径
                    userFileDTO.setFileName(item.objectName().split("/")[1]);             // 文件名
                    userFileDTO.setFileSize(item.size());                                       // 文件大小
                    Instant instant = item.lastModified().toInstant();
                    Date date = Date.from(instant);
                    userFileDTO.setLastModified(sdf.format(date));                              // 最后修改时间

                    userFiles.add(userFileDTO);
                }
            }
        } catch (Exception e) {
            System.out.println(e);
            return R.OK("获取文件列表失败");
        }

        for (UserFileDTO userFile : userFiles) {
            System.out.println(userFile);
        }

        return R.OK(userFiles);
    }

    /**
     * 普通上传文件到minio
     * @param file 用户要上传的文件
     * @param userId // 用户id
     * @return
     * @throws Exception
     */
    @PostMapping("/poolUploadFile")
    public R image(@RequestParam("file") MultipartFile file,
                   @RequestParam("userId") String userId) throws Exception {

        System.out.println("前端传递过来的文件：" + file.getOriginalFilename());
        System.out.println("前端传递过来的userId：" + userId);
        // String subfix = file.getOriginalFilename().substring(file.getOriginalFilename().indexOf("."));

        // 以用户ID + 源文件名作为minio中存储的文件名
        String fileName = userId + "/" + file.getOriginalFilename();

        // bucketExists() 用于检查指定的存储桶是否存在，返回布尔值，表示存储桶是否存在；
        boolean isBucketExists = minioClient.bucketExists(
                // BucketExistsArgs.builder().bucket("my-file").build()
                BucketExistsArgs.builder().bucket(minioConfig.getBucket()).build()
        );
        if (!isBucketExists) {
            // 如果当前桶不存在，则创建桶
            // minioClient.makeBucket(MakeBucketArgs.builder().bucket("my-file").build());
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioConfig.getBucket()).build());
        }

        // 上传文件到minio
        ObjectWriteResponse objectWriteResponse = minioClient.putObject(PutObjectArgs.builder()
                // .bucket("my-file")
                .bucket(minioConfig.getBucket())
                .object(fileName)
                .stream(file.getInputStream(), file.getSize(), -1) // 最后一个参数是缓冲区大小，给-1让其自己决定
                .build());
        System.out.println(objectWriteResponse);
        System.out.println(objectWriteResponse.etag());

        return R.OK(fileName);
    }

    /**
     * 分块上传
     * @param file 分块文件
     * @param userId 用户id
     * @param fileMd5 文件的Md5值
     * @param chunkIndex 当前分块的块数
     * @return
     */
    @PostMapping("/chunkUploadFile")
    public R chunkUploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam("userId") String userId,
            @RequestParam("fileMd5") String fileMd5,
            @RequestParam("chunkIndex") Integer chunkIndex
            ) {

        String directName = userId + "/" + fileMd5 + "/chunk/" + chunkIndex;

        // 1、接收到前端传递过来的文件的分块，先去查看minio中是否有当前分块，如果有并且分块完整，则不需要再上传，直接返回给前端上传成功
        try {
            StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder()
                    .bucket(minioConfig.getBucket())
                    .object(directName) // 分块文件路径
                    .build());

            if (statObjectResponse != null) {
                // 说明当前分块存在
                System.out.println("当前第" + chunkIndex + "分块已存在, 分块大小为：" + statObjectResponse.size());
                // 如果当前传入的文件与minio中存储的分块大小一致，则不需要上传
                if (file.getSize() == statObjectResponse.size()) {
                    return R.OK("分块已存在，不需要上传");
                } else {
                    // 如果当前分块不存在，或者当前分块并的大小与上传的文件的大小不一致(并不是5MB)，则重新上传该分块到MinIO
                    // 当调用 putObject 上传文件时，如果目标 bucket 中已存在相同名称的对象，新上传的文件会自动替换旧文件
                    minioClient.putObject(PutObjectArgs.builder()
                            .bucket(minioConfig.getBucket())
                            .object(directName)
                            .stream(file.getInputStream(), file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build());
                }
            }
        } catch (ErrorResponseException e){ // ErrorResponse(code = NoSuchKey, message = Object does not exist, bucketName = big-file-bucket, objectName = 123/782fee462cc645cbf7747b1158846da9/chunk/24, resource =
            // 走到这里说明当前分块并不存在，上传即可
            try {
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(minioConfig.getBucket())
                        .object(directName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());
            } catch (Exception exception) {
                e.printStackTrace();
                return R.FAIL("分块上传失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("出错啦！" + e.getMessage());
            return R.FAIL("异常错误出现！" + e.getMessage());
        }

        return R.OK("当前分块上传成功");
    }

    /**
     * 合并分块文件
     * @return
     */
    @PostMapping("/mergeChunks")
    public R mergeChunks(@RequestBody MergeFileDTO mergeFileDTO){

        // 准备合并的分块列表
        // 将分块文件组成 List<ComposeSource>（找到所有分块文件）
        List<ComposeSource> sources = new ArrayList<>();
        for (int i = 0; i < mergeFileDTO.getTotalChunks(); i++) {
            String chunkName = mergeFileDTO.getUserId() + "/" + mergeFileDTO.getFileMd5() + "/chunk/" + i;
            sources.add(ComposeSource.builder()
                            .bucket(minioConfig.getBucket())
                            .object(chunkName)
                            .build());
        }

        System.out.println("分块列表准备完毕！");

        // 合并后的文件名
        String mergeFileName = mergeFileDTO.getUserId() + "/" + mergeFileDTO.getFileMd5() + "/" + mergeFileDTO.getFileName();

        // 执行合并
        try {
            ObjectWriteResponse objectWriteResponse = minioClient.composeObject(ComposeObjectArgs.builder()
                    .bucket(minioConfig.getBucket())
                    .object(mergeFileName)
                    .sources(sources)
                    .build());

            System.out.println("分块合并成功！");
            System.out.println(objectWriteResponse.etag());
        } catch (Exception e) {
            System.out.println("分块合并失败, " + e.getMessage());
        }

        // 合并完成后删除临时分块
        try {
            for (int i = 0; i < mergeFileDTO.getTotalChunks(); i++) {
                String chunkName = mergeFileDTO.getUserId() + "/" + mergeFileDTO.getFileMd5() + "/chunk/" + i;
                minioClient.removeObject(RemoveObjectArgs.builder()
                                .bucket(minioConfig.getBucket())
                                .object(chunkName)
                                .build());
            }
            System.out.println("删除所有分块成功！");
        } catch (Exception e) {
            System.out.println("删除分块失败, " + e.getMessage());
        }

        // 合并文件成功！
        return R.OK(mergeFileName);
    }

    /**
     * 文件下载
     */
    @GetMapping("/downloadFile")
    public void downloadFile(
            @RequestParam("objectName") String objectName,
            HttpServletResponse response){

        try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
                .bucket(minioConfig.getBucket())
                .object(objectName)
                .build())) {

            // 设置响应头
            // setContentType：作用: 设置响应的内容类型
            response.setContentType("application/octet-stream"); // 表示这是一个二进制流文件，这是通用的二进制文件类型，浏览器会将其识别为需要下载的文件
            // setHeader：作用: 控制浏览器如何处理响应内容
            // attachment 表示浏览器应该下载文件而不是尝试显示它
            // filename 指定下载时保存的默认文件名（双引号包裹文件名是为了处理文件名中包含空格或特殊字符的情况）
            // 重要性: 如果没有这个头，浏览器可能会尝试直接显示文件内容而不是触发下载
            response.setHeader("Content-Disposition", "attachment; filename=\"" + objectName + "\"");
            // 缓存控制头，下方代码总体作用是：禁用缓存
            //     Cache-Control:
            //          no-cache: 不直接使用缓存，必须先向服务器验证
            //          no-store: 不存储任何缓存
            //          must-revalidate: 缓存必须在使用前验证其新鲜度
            // Pragma: HTTP/1.0的遗留头，no-cache用于向后兼容
            // Expires: 设置为0表示资源已过期
            response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Expires", "0");

            /*
            * 为什么需要禁用缓存:
                确保用户总是获取最新的文件版本
                防止浏览器缓存敏感文件
                避免下载旧版本文件的问题
            * */

            // 将文件流写入响应
            try (OutputStream os = response.getOutputStream()) {
                byte[] buffer = new byte[1024]; // 1024字节的缓冲区
                int bytesRead = -1;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    os.write(buffer, 0, bytesRead);
                }

                // 作用: 强制将任何缓冲的输出字节写入底层流
                // 重要性:
                //  确保所有数据都被发送到客户端
                //  在关闭流之前刷新可以防止数据丢失
                os.flush();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @DeleteMapping("/deleteFile")
    public R deleteFile(@RequestParam("objectName") String objectName){
        try {
            minioClient.removeObject(RemoveObjectArgs.builder()
                            .bucket(minioConfig.getBucket())
                            .object(objectName)
                            .build());
        } catch (Exception e) {
            return R.FAIL("文件删除失败");
        }

        return R.OK("文件删除成功");
    }

}